<html>
<head>
  <title>使用 Python 和 Asyncio 编写在线多人游戏</title>
  <basefont face="微软雅黑" size="2" />
  <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
  <meta name="exporter-version" content="Evernote Windows/307027 (zh-CN, DDL); Windows/6.1.0 (Win32);"/>
  <style>
    body, td {
      font-family: 微软雅黑;
      font-size: 10pt;
    }
  </style>
</head>
<body>
<a name="623"/>
<h1>使用 Python 和 Asyncio 编写在线多人游戏</h1>

<div>
<span><div><h3><span style="font-weight: bold;"><font style="font-size: 18pt; color: rgb(28, 51, 135);">简介</font></span></h3><div><span style="font-weight: bold; font-size: 14pt;">基于动态语言的现代框架允许在中档的硬件上面处理大量并发的用户连接。同时，HTML5 和 WebSockets 标准使得创建基于实时图形的游戏的直接运行至浏览器上的客户端成为可能，而不需要任何的扩展。</span></div><div><a href="https://docs.python.org/3/library/asyncio.html" style="font-weight: bold; font-size: 14pt;">asyncio</a> <span style="font-weight: bold; font-size: 14pt;">的引入和一个特别的</span> <a href="https://docs.python.org/3/whatsnew/3.5.html#whatsnew-pep-492" style="font-weight: bold; font-size: 14pt;">async/await</a> <span style="font-weight: bold; font-size: 14pt;">语法使得异步代码看起来像常规的阻塞代码一样，这使得 Python 成为了一个值得信赖的异步编程语言，所以我将尝试利用这些新特点来创建一个多人在线游戏。</span></div><div><span style="font-weight: bold; font-size: 14pt;"><br/></span></div><h3><font style="font-size: 18pt; color: rgb(28, 51, 135);">异步</font></h3><div><span style="font-weight: bold; font-size: 14pt;">这就是为什么出现微线程（microthread）概念的原因。这个想法是为了在一个线程上并发执行任务。当你在一个任务中调用阻塞的方法时，有一个叫做“manager” (或者“scheduler”)的东西在执行事件循环。当有一些事件准备处理的时候，一个 manager 会转移执行权给一个任务，并等着它执行完毕。任务将一直执行，直到它遇到一个阻塞调用，然后它就会将执行权返还给 manager。</span></div><div><span style="font-weight: bold; font-size: 14pt;"><br/></span></div><h3><font style="font-size: 18pt; color: rgb(28, 51, 135);">编写游戏循环主体</font></h3><div><font style="font-size: 14pt; color: rgb(50, 135, 18);"><b>游戏循环是每一个游戏的核心。它持续地运行以读取玩家的输入、更新游戏的状态，并且在屏幕上渲染游戏结果。</b></font></div><div><span style="line-height: 1.45;"><br/></span></div><div><font style="font-size: 12pt; color: rgb(28, 51, 135);"><b>在在线游戏中，游戏循环分为客户端和服务端两部分，所以一般有两个循环通过网络通信。</b></font></div><div><span style="line-height: 1.45;"><br/></span></div><div><ul><li><span style="line-height: 1.45;"><font style="font-size: 16pt; color: rgb(173, 0, 0);"><b>客户端的角色</b></font></span></li></ul></div><div>获取玩家输入，比如按键或者鼠标移动，将数据传输给服务端，然后接收需要渲染的数据。</div><div><ul><li><font style="font-size: 16pt; color: rgb(173, 0, 0);"><b>服务端的角色</b></font></li></ul></div><div><ol><li>处理来自玩家的所有数据</li><li>更新游戏的状态</li><li>执行渲染下一帧的必要计算</li><li>将结果传回客户端（例如游戏中对象的新位置）</li></ol></div><div><span style="line-height: 1.45;"><br/></span></div><div><img src="使用 Python 和 Asyncio 编写在线多人游戏_files/Image.png" type="image/png" data-filename="Image.png"/></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><h4><font style="font-size: 18pt; color: rgb(28, 51, 135);">基本游戏循环</font></h4><div>我们使用 <a href="http://aiohttp.readthedocs.org/">aiohttp</a> 库来创建游戏服务器。它可以通过 asyncio 创建网页服务器和客户端。这个库的一个优势是它同时支持普通 http 请求和 websocket。所以我们不用其他网页服务器来渲染游戏的 html 页面。</div><div style="box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902); background-position: initial initial; background-repeat: initial initial;-en-codeblock:true;"><div>app = web.Application()</div><div>app[&quot;sockets&quot;] = []</div><div>asyncio.ensure_future(game_loop(app))</div><div>app.router.add_route('GET', '/connect', wshandler)</div><div>app.router.add_route('GET', '/', handle)</div><div>web.run_app(app)</div></div><div><span style="line-height: 1.45;"><br/></span></div><div>app 变量就是一个类似于字典的对象，它用于在所连接的客户端之间共享数据。</div><div>asyncio.ensure_future() 调用会启动主游戏循环的任务，每隔2 秒向客户端发送嘀嗒消息。这个任务会在同样的 asyncio 事件循环中和网页服务器并行执行。</div><div><span style="line-height: 1.45;"><br/></span></div><div><font style="font-size: 12pt; color: rgb(173, 0, 0);"><b>有两个网页请求处理器：</b></font></div><div><ul><li>handle 是提供 html 页面的处理器；</li><li>wshandler 是主要的 websocket 服务器任务</li></ul></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><h4><font style="font-size: 18pt; color: rgb(28, 51, 135);">根据请求启动游戏</font></h4><div><span>    <span>    </span></span>我们使用一个全局标记来检测游戏循环是否在执行。当第一个用户发起连接时，启动它。最开始，游戏循环没有执行，标记设置为 False。游戏循环是通过客户端的处理方法启动的。</div><div style="box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902); background-position: initial initial; background-repeat: initial initial;-en-codeblock:true;"><div>if app[&quot;game_is_running&quot;] == False</div><div><span>    asyncio.ensure_future(game_loop(app))</span><br/></div></div><div><span>    <span>    </span></span>当 game_loop() 运行时，这个标记设置为 True；当所有客户端都断开连接时，其又被设置为 False。</div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><h4><font style="font-size: 18pt; color: rgb(28, 51, 135);">管理任务</font></h4><div>我们把游戏循环的任务直接存储在游戏循环的全局字典中，代替标记的使用。</div><div style="box-sizing: border-box; padding: 8px; font-family: Monaco, Menlo, Consolas, &quot;Courier New&quot;, monospace; font-size: 12px; color: rgb(51, 51, 51); border-top-left-radius: 4px; border-top-right-radius: 4px; border-bottom-right-radius: 4px; border-bottom-left-radius: 4px; background-color: rgb(251, 250, 248); border: 1px solid rgba(0, 0, 0, 0.14902); background-position: initial initial; background-repeat: initial initial;-en-codeblock:true;"><div>if app[&quot;game_loop&quot;] is None or \</div><div><span>    </span>app[&quot;game_loop&quot;].cancelled():</div><div><span>    </span>app[&quot;game_loop&quot;] = asyncio.ensure_future(game_loop(app))</div></div><div>这里 ensure_future() 返回我们存放在全局字典中的任务对象，当所有用户都断开连接时，我们使用下面方式取消任务：</div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div><div><span style="line-height: 1.45;"><br/></span></div></div></span>
</div></body></html> 